/*
 * Copyright 2010 Alessio Stalla.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dynaspring;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashSet;
import java.util.Set;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternUtils;

/**
 * A BeanDefinitionReader that reads bean definitions from Lisp source files. This is the Java side of DynaSpring's core engine.
 * Users typically won't use this class directly.
 * If asked to load a resource with .xml extension, this class delegates to an XmlBeanDefinitionReader.
 * @author Alessio Stalla
 *
 */
public class LispBeanDefinitionReader extends AbstractBeanDefinitionReader {

	private static final ScriptEngine scriptEngine;
	private boolean compile = false;
	private static final Log log = LogFactory.getLog(LispBeanDefinitionReader.class);
	private Configuration configuration = new Configuration();
	private final Set<Resource> loadedResources = new HashSet<Resource>();
	
	static {
		ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("lisp");
		if(engine == null) {
			throw new RuntimeException("No lisp ScriptEngine found, is ABCL in your classpath?");
		}
		try {
			loadCompiledLispResource((Compilable) engine, "packages.lisp", DynamicRefreshableApplicationContext.class);
			loadCompiledLispResource((Compilable) engine, "dynamic-spring.lisp", DynamicRefreshableApplicationContext.class); 
		} catch (Exception e) {
			throw new RuntimeException("Couldn't initialize dynamic spring", e);
		} 
		scriptEngine = engine;
	}
	
	/**
	 * Dummy method to ensure this class is resolved and the Lisp side of DynaSpring is loaded.
	 */
	public static void init() {}

	public LispBeanDefinitionReader(BeanDefinitionRegistry registry) {
		super(registry);
	}
	
	public LispBeanDefinitionReader(BeanDefinitionRegistry registry, Configuration configuration) {
		super(registry);
		this.configuration = configuration;
	}

	@Override
	public int loadBeanDefinitions(Resource res) throws BeanDefinitionStoreException {
		try {
			return doLoadBeanDefinitions(res);
		} catch(IOException e) {
			throw new BeanDefinitionStoreException("IOException when reading bean definitions from " + res, e);
		}
	}
	
	public int requireBeanDefinitions(Resource res) {
		if(!loadedResources.contains(res)) {
			return loadBeanDefinitions(res);
		} else {
			if(log.isDebugEnabled()) { log.debug(res + " already loaded"); }
			return 0;
		}
	}
	
	protected int doLoadBeanDefinitions(Resource res) throws IOException {
		log.info("Loading DynaSpring Lisp bean definitions from " + res);
		String resUrl = res.getURL().toString();
		if(resUrl.endsWith(".lisp")) {
			//TODO
			/*if(resUrl.endsWith(".lisp")) {
				int len = resUrl.length();
				Resource candidateCompiledResource = new UrlResource(resUrl.substring(len - ".lisp".length()) + ".abcl");
				if(candidateCompiledResource.exists() && candidateCompiledResource.lastModified() > res.lastModified()) {
					res = candidateCompiledResource;
				}
			}*/
			Reader r = new InputStreamReader(res.getInputStream());
			try {
				int loaded = loadBeanDefinitions(r, res);
				loadedResources.add(res);
				return loaded;
			} finally {
				r.close();
			}
		} else if(resUrl.endsWith(".xml")) {
			XmlBeanDefinitionReader r = new XmlBeanDefinitionReader(getBeanFactory());
			int loaded =  r.loadBeanDefinitions(res);
			loadedResources.add(res);
			return loaded;
		} else {
			throw new RuntimeException("Unsupported resource: " + res);
		}
	}

	public int loadBeanDefinitions(Reader r, Resource res) throws IOException {
		BeanDefinitionRegistry factory = getBeanFactory();
		int initialCount = factory.getBeanDefinitionCount();
		Bindings b = scriptEngine.createBindings();
		b.putAll(scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE));
		b.putAll(scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE));
		b.putAll(getConfiguration().getVariables());
		b.put("spring::*load-resource*", res);
		b.put("spring::*bean-definition-reader*", this);
		b.put("spring::*bean-factory*", factory);
		try {
			b.put("cl:*package*", getInvocable().invokeFunction("cl:find-package", "SPRING"));
		} catch (Exception e) {
			log.error("Couldn't bind *package* to :spring", e);
		}
		try {
			if(compile) {
				getCompilable().compile(r).eval(b);
			} else {
				scriptEngine.eval(r, b);
			}
		} catch (ScriptException e) {
			throw new ApplicationContextException("Error loading beans from " + r, e);
		}
		return factory.getBeanDefinitionCount() - initialCount;
	}
	
	public int loadBeanDefinitions(Resource parentRes, String location) throws IOException {
		Resource res = getResource(parentRes, location);
		try {
			return loadBeanDefinitions(res);
		} catch (BeanDefinitionStoreException ex) {
			logger.error("Failed to import bean definitions from " + res,  ex);
			throw ex;
		}
	}
	
	/**
	 * Loads bean definitions from the specified location, only if the corresponding resource hasn't yet been loaded
	 * by this BeanDefinitionReader.
	 * @param parentRes
	 * @param location
	 * @return
	 * @throws IOException
	 */
	public int requireBeanDefinitions(Resource parentRes, String location) throws IOException {
		Resource res = getResource(parentRes, location);
		try {
			return requireBeanDefinitions(res);
		} catch (BeanDefinitionStoreException ex) {
			logger.error("Failed to import bean definitions from " + res,  ex);
			throw ex;
		}
	}

	public Resource getResource(Resource parentRes, String location) throws IOException {
		location = configuration.replacePlaceholders(location, false);
		Resource res;
		if (ResourcePatternUtils.isUrl(location) || parentRes == null || location.startsWith("/")) {			
			res = getResourceLoader().getResource(location);
		} else {
			res = parentRes.createRelative(location);
		}
		return res;
	}
	
	public static void loadCompiledLispResource(String name, Class<?> refClass) throws ScriptException, IOException {
		loadCompiledLispResource((Compilable) scriptEngine, name, refClass);
	}
	
	public static void loadCompiledLispResource(Compilable compiler, String name, Class<?> refClass) throws ScriptException, IOException {
		loadCompiledLispResource(compiler, new ClassPathResource(name, refClass));
	}
	
	public static void loadCompiledLispResource(Resource resource) throws ScriptException, IOException {
		loadCompiledLispResource((Compilable) scriptEngine, resource);
	}
	
	public static void loadCompiledLispResource(Compilable compiler, Resource resource) throws ScriptException, IOException {
		log.info("Loading DynaSpring code from " + resource);
		Reader r = new InputStreamReader(resource.getInputStream());
		compiler.compile(r).eval();
		r.close();
	}
	
	public boolean isCompile() {
		return compile;
	}

	public void setCompile(boolean compile) {
		this.compile = compile;
	}
	
	public static ScriptEngine getScriptEngine() {
		return scriptEngine;
	}
	
	public static Invocable getInvocable() {
		return (Invocable) scriptEngine;
	}
	
	public static Compilable getCompilable() {
		return (Compilable) scriptEngine;
	}
	
	public Configuration getConfiguration() {
		return configuration;
	}
	
	/**
	 * Registers a variable that can be read in Lisp code.
	 * @param name the name of the variable.
	 * @param value the value.
	 */
	public void put(String name, Object value) {
		scriptEngine.put(name, value);
	}

}
